Skip to content

fix(advisor): every association count names its population (FM5) - #17

Merged
stranske merged 1 commit into
mainfrom
claude/skill-advisor-wiring
Aug 22, 2026
Merged

fix(advisor): every association count names its population (FM5)#17
stranske merged 1 commit into
mainfrom
claude/skill-advisor-wiring

Conversation

@stranske

Copy link
Copy Markdown
Owner

Why

capability_advisor.learned_associations() — the reporting path of the tool that answers "are the
skills actually wired to the front door?"
— computed observations as the sum over by_skill
alone. An advisory match recorded without a skill was therefore invisible in the count while
still being counted in by_task_type.

On the live ledger that reads:

by_task_type:  testgen 6+5, codemod 3, docs 2, offload 2, review 1, runtime_ac 1   (20 events)
observations:  11                                                                  <- the bug

The gap looks like a ledger bug. It is a denominator bug: FM5 (convenient denominators), sitting in
the reporting path of the very tool meant to measure the thing. Measured with that number, "the
skills are wired now" is unfalsifiable — the count can only ever move for the population it already
covers, so a rise proves nothing and a flat line hides everything.

What changed

capability_advisor.py only. Three counts that each name their population and reconcile by
construction, plus a populations map so a subset cannot pass as the set:

key population
observations every capability_advisor match event in the ledger
observations_with_skill the subset naming a skill — the only population by_skill can cover
observations_without_skill matches with no skill: counted in by_task_type, absent from by_skill

with_skill + without_skill == observations is asserted. Live ledger now reads 20 / 11 / 9 where
the old code reported a bare 11.

No behaviour change to classification, matching, recording or dispatch. No new store, no second
advisor, no lifecycle record touched — this is the existing surface reporting honestly.

Test gate

Named assertions added to capability_advisor._selftest(): an advisory call with no skill (what
a session that forgets skill= produces) must be absent from by_skill, visible in the totals, and
must make observations > observations_with_skill hold.

Deliberate break → revert, performed: restoring "observations": with_skill fails the new
assertion and prints the bug in the failure message — observations: 2 beside
by_task_type testgen: 3, the live symptom in miniature. The old definition made those two equal, so
the assertion cannot pass without the fix. Revert is byte-identical.

Verification

  • python3 verify.py green on this branch.
  • python3 capability_advisor.py --selftest green; python3 capabilities.py --selftest and
    --json validate green (41 rows, "valid": true, 0 errors).
  • State-independent: passes with both ORCH_STATE_DIR and ORCH_LOCAL_RUNTIME pointed at empty
    directories; the front-door section skips with the prerequisite named. No new skip, floor
    unchanged.

Non-goals

  • Not widening TASK_SIGNALS or any capability matcher. Raising the hit rate so more things match
    would destroy the signal the counts exist to carry (design commitment Configure Renovate #1: the advisor must be able
    to say no).
  • No lifecycle record created for capability_advisor itself: it is an observer/reporting surface,
    and registering it would force a declared delivery outcome it cannot physically produce.
  • Mirror not synced — that stays the owner's manual circuit breaker.

Context

Companion to a ~/.claude/skills change (machine-local, not in this repo): seven skills now consult
the capability_advice MCP tool at task initiation with the user's real task text, taking by_skill
from 1 distinct skill to 5. Hitting this denominator while measuring that is what surfaced the bug.

🤖 Generated with Claude Code

`learned_associations()` computed `observations` as the sum over `by_skill`
ALONE, so an advisory match recorded WITHOUT a skill was invisible in the
count while still being counted in `by_task_type`. The live ledger read
"observations: 3" beside 12 task-type observations, and the gap looked like a
ledger bug rather than a denominator bug.

That is a convenient denominator sitting in the reporting path of the very
tool meant to measure whether the skills are wired to the front door. Measured
with that number, "the skills are wired now" is unfalsifiable — the count can
only ever move for the population it already covers.

Three counts that each name their population and reconcile by construction:

  observations               every capability_advisor match event
  observations_with_skill    the subset naming a skill — the ONLY population
                             by_skill can cover
  observations_without_skill counted in by_task_type, absent from by_skill

plus a `populations` map so a subset cannot pass as the set. On the live
ledger this reads 20 / 11 / 9 where the old code reported a bare "11".

Selftest: a skill-less advisory match must be visible in the totals and absent
from by_skill, and `observations > observations_with_skill` must hold — the old
definition made those two equal. Deliberate break -> revert performed:
restoring `"observations": with_skill` fails the new assertion and prints the
bug in the failure message (observations 2 beside by_task_type testgen 3, the
live symptom in miniature); the revert is byte-identical.

No behaviour change to classification, matching, recording or dispatch. No new
store, no second advisor, no lifecycle record touched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 22, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your current included review allowance is based on your included PR review attempts over the past 7 days.

Next review available in: 25 minutes

Limit details: You’ve used the included review currently available. Your 66 included PR review attempts over the past 7 days set your current allowance at 1 review per hour.

Your organization has reached its usage spending cap. Adjust your spending cap in the billing tab.

How can I continue?

Wait for the limit to reset, then comment @coderabbitai review or push new commits to the PR.

An organization admin can change what happens after included review limits in Billing.

How do review limits work?

CodeRabbit enforces per-developer PR review limits within each organization.

For paid Pro and Pro+ reviews, CodeRabbit uses a developer's included PR review attempts over the past 7 days to set the current hourly allowance. At typical activity levels, the full plan allowance applies. Higher sustained activity can lower the allowance until earlier attempts leave the 7-day window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 76eaa54a-2537-47eb-8d6e-db1732859fa2

📥 Commits

Reviewing files that changed from the base of the PR and between cdc9367 and 5e55b0a.

📒 Files selected for processing (1)
  • capability_advisor.py

Comment @coderabbitai help to get the list of available commands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant